import time

import pandas as pd
import matplotlib.pyplot as plt


start_time = time.time()


'''数据加载'''
# 加载数据集，打开评分文件、电影信息文件、个人评分文件
ratings_object = open('movieData/ratings.dat')
movies_object = open('movieData/movies.dat')
personal_object = open('movieData/personalRatings2.txt')


# 读取数据集
ratings_list = []
all_ratings_list = []
for item in (line.split('::') for line in ratings_object.read().split('\n')):       # 按行读取文件，利用冒号作为分隔符
    ratings_list.append(item[2])        # 评分数据加载到ratings_list数组中
    all_ratings_list.append(item)       # 整个评分文件放到list中
print(ratings_list)
person_list = []
for item in (line.split('::') for line in personal_object.read().split('\n')):       # 按行读取文件，利用冒号作为分隔符
    if int(item[2]) > 3:
        person_list.append(int(item[1]))        # 用户数据加载到person_list数组中
movies_list = []
for item in (line.split('::') for line in movies_object.read().split('\n')):       # 按行读取文件，利用冒号作为分隔符
    if item[1] not in movies_list:
        movies_list.append(item[1])        # 电影数据加载到movies_list数组中


'''以ratings.dat文件为例，画出异常数据'''
data = pd.read_csv('movieData/ratings.dat', sep='::', engine='python')      # 读入ratings文件
df = pd.DataFrame(data)     # 转化成dataframe文件
df.columns = ['user_id', 'film_id', 'star', 'time']     # 添加列名
df_stars = df['star']       # 读入评分数据，1-5分为正常值
print(df_stars)
# print(df_stars.describe())
fig, ax = plt.subplots(1, 1)
df_stars.plot.box(title="rating in Movies", ax=ax, ylim=(-5, 20))       # 画出箱型图查看异常值
plt.grid(linestyle='--', alpha=0.3)
plt.show()


'''缺失值处理'''
# 删除ratings.dat中异常评分数据
for line in ratings_list:       # 若不在正常分值范围内则替换
    if line not in range(1, 6):      # 检测评分分值是否在1-5之间
        line = 3      # 不在该范围内的评分替换为中值3


'''数据挖掘'''


# 假设最小支持度设置为0.5，dataset存放的为事务数据库的信息。首先调用createC1（）来创建初始候选项集C1，
# 接着调用scanD（）来根据候选项集创建频繁项集。然后进入循环处理，直到没有新的频繁项集产主为止。
def apriori(dataSet,minSupport =0.5):
    # 构建初始候选项集c1
    C1 = createC1(dataSet)
    # 将dataset集合化，以满足scanD的格式要求
    D = list(map(set,dataSet))
    # 构建初始的频繁项集，即所有项集只有一个元素
    L1, suppData = scanD(D,C1,minSupport)
    L = [L1]
    # 最初的11中的每个项集含有一个元素，新生成的项集应该含有2个元素，所以k=2
    k =2
    while(len(L[k-2]) > 0):
        Ck  = aprioriGen(L[k-2],k)
        Lk,supK = scanD(D,Ck,minSupport)
        # 将新的项集的支持度数据加入原来的总支持度字典中
        suppData.update(supK)

        # 将符合最小支持度要求的项集加入L
        L.append(Lk)
        k += 1
        return L, suppData


# 构建初始候选项集的列表
def createC1(dataSet):
    C1 = []
    # 即所有候选项集只包含一个元素，
    for transaction in dataSet:
        for item in transaction:
    #C1是大小为1的所有候选项集的集合。
            if [item] not in C1:
                C1.append([item])
    C1.sort()
    return list(map(frozenset, C1))


# 遍历函数
def scanD(D,Ck,minSupport):
    # 计算Ck中的项集在事务集合D的每个transactions中的支持度,
    # 返回满足最小支持度的项集的集合，和所有项集支持度信息的字典。
    ssCnt = {}
    for tid in D:
        # 对于每一条transaction
        for can in Ck:
            if can.issubset(tid):
                ssCnt[can] = ssCnt.get(can, 0) + 1
    numItems = float(len(D))
    retList = []
    supportData = {}
    # 数据集中的每个样本，候选项集中的每个集合，
    # 如果该集合为样本子集，那么对应计数+1
    # 遍历ssCnt字典
    for key in ssCnt: # 每个项集的支持度
        support = ssCnt[key] / numItems # 将满足最小支持度的项集，加入retList
        if support >= minSupport:
           retList.insert(0, key) # 汇总支持度数据
        supportData[key] = support
    return retList, supportData


# 频繁项集合生成代码
# 算法具体实现！
def aprioriGen(Lk,k):
    # k表示生成的新项集中所含有的元素个数
    retList = [] # 由初始候选项集的集合Lk生成新的生成候选项集，
    lenLk = len(Lk)# k表示生成的个数
    for i in range( lenLk ):
        for j in range( i + 1, lenLk ):
            L1 = list( Lk[ i ] )[ : k - 2 ]     # 一项集合
            L2 = list( Lk[ j ] )[ : k - 2 ]     # 二项集合
            L1.sort();L2.sort()
            if L1 == L2:
                retList.append( Lk[ i ] | Lk[ j ] )
# 如果这两个集合的前面k-2个元素都相等，那么就将这两个集合合成一个大小为k的集合
    return retList      # 返回候选集


# 规则生成函数
# 根据频繁项集和最小可信度生成规则。L(list):存储频繁项集
# supportData(dict):存储着所有项集（不仅仅是频繁项集）的支持度 minConf(float):最小可信度
def generateRules( L, supportData, minConf=0.5 ):
    bigRuleList = []
    for i in range( 1, len( L ) ):
        for freqSet in L[ i ]:
            # 对于每一个频繁项集的集合freqSet
            H1 = [ frozenset( [ item ] ) for item in freqSet ]
            # 如果频繁项集中的元素个数大于2，需要进一步合并
            if i > 1:
                rulesFromConseq( freqSet, H1, supportData, bigRuleList, minConf )
            else:
                calcConf( freqSet, H1, supportData, bigRuleList, minConf )
    return bigRuleList


# 关联规则生成代码
 # 对频繁项集中元素超过2的项集进行合并。 freqSet(frozenset):频繁项集
# H(frozenset):频繁项集中的所有元素，即可以出现在规则右部的元素，supportData(dict):所有项集的支持度信息。brl(tuple):生成的规则
def rulesFromConseq(freqSet, H, supportData, brl, minConf=0.5):
    m = len(H[0])
    if m == 1:
        calcConf(freqSet, H, supportData, brl, minConf)
    # 查看频繁项集是否大到移除大小为m的子集
    if len(freqSet) > m + 1:
        Hmp1 = aprioriGen(H, m + 1)
        Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf)
        # 如果不止一条规则满足要求，进一步递归合并
        if len(Hmp1) > 1:
            rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)


# 计算规则的可信度，返回满足最小可信度的规则。 freqSet(frozenset):频繁项集
# H(frozenset):频繁项集中所有的元素supportData(dic):频繁项集中所有元素的支持度
# brl(tuple):满足可信度条件的关联规则 minConf(float):最小可信度
def calcConf(freqSet,H,supportData,brl,minConf=0.5):
    prunedH = []
    for conseq in H:#freqSet(frozenset):频繁项集
        conf = supportData[freqSet] / supportData[freqSet - conseq]
        lift=conf/supportData[conseq]#H(frozenset):频繁项集中所有的元素
        if conf >= minConf:
            # print (freqSet - conseq, '-->', conseq, 'conf:', conf)
            brl.append((freqSet - conseq, conseq, conf))
            prunedH.append(conseq)
    return prunedH


# 根据已经生成的规则，遍历用户个人喜欢的电影列表，按照已生成的规则来进行推荐。
def recommendMovies(rules,personal_list,movie_list):
    recommend_list = [] # 推荐列表
    sup_list = []   # 支持
    for rule in rules:  # 遍历规则
        if rule[0] <= personal_list:
            for movie in rule[1]:
                if movie_list[movie-1] not in recommend_list:
                    recommend_list.append(movie_list[movie-1])
                    sup_list.append(rule[2])
    for recommend in recommend_list:
        i = recommend_list.index(recommend)
        print('推荐观看电影', recommend, ',', round(sup_list[i]*100,2), '% 和你相似的人!')


del_ratings = []        # 数组中存放评分大于3的数组
for i in all_ratings_list:
    if int(i[2]) > 3:
        del_ratings.append(i)        # 将所有评分小于等于3的数据扔掉
print(del_ratings)

unique_ratings = []     # 数组中存放唯一的用户id和他喜欢的电影
count = 1
list2 = []
for i in del_ratings:
    if int(i[0]) == count:      # count作为用户表示id，相同的用户电影放在一个事务中
        list2.append(int(i[1]))     # 将str类型的电影名转化为int类型加到数组中
        continue
    unique_ratings.append(list2)
    count += 1
    list2 = []
print(unique_ratings)

L, rule_list = apriori(unique_ratings, 0.2)     # 挖掘频繁项集
rules = generateRules(L, rule_list, 0.5)        # 挖掘强关联规则
print(rule_list)
print(rules)

recommendMovies(rules, frozenset(person_list), movies_list)     # 根据强关联规则挖掘相关用户的爱好
